Configure continuous integration pipelines for Evernote integrations, including test automation, credential management, and deployment workflows.
# .github/workflows/evernote-ci.yml
name: Evernote Integration CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
NODE_VERSION: '20'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm run test:unit
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
integration-tests:
runs-on: ubuntu-latest
needs: [lint, unit-tests]
# Only run on main branch to preserve rate limits
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run integration tests
env:
EVERNOTE_CONSUMER_KEY: ${{ secrets.EVERNOTE_CONSUMER_KEY }}
EVERNOTE_CONSUMER_SECRET: ${{ secrets.EVERNOTE_CONSUMER_SECRET }}
EVERNOTE_ACCESS_TOKEN: ${{ secrets.EVERNOTE_SANDBOX_TOKEN }}
EVERNOTE_SANDBOX: 'true'
run: npm run test:integration
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Snyk security scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high
// jest.config.js
module.exports = {
testEnvironment: 'node',
testMatch: [
'**/tests/**/*.test.js'
],
collectCoverageFrom: [
'src/**/*.js',
'!src/**/*.test.js'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
},
setupFilesAfterEnv: ['./tests/setup.js'],
// Separate test suites
projects: [
{
displayName: 'unit',
testMatch: ['<rootDir>/tests/unit/**/*.test.js']
},
{
displayName: 'integration',
testMatch: ['<rootDir>/tests/integration/**/*.test.js'],
testTimeout: 30000
}
]
};
// tests/setup.js
const dotenv = require('dotenv');
// Load test environment
dotenv.config({ path: '.env.test' });
// Global test setup
beforeAll(() => {
// Verify test environment
if (process.env.NODE_ENV === 'production') {
throw new Error('Cannot run tests in production environment');
}
// Ensure sandbox mode for Evernote tests
if (process.env.EVERNOTE_SANDBOX !== 'true') {
console.warn('WARNING: EVERNOTE_SANDBOX should be true for tests');
}
});
// Global teardown
afterAll(async () => {
// Clean up test resources
});
// tests/mocks/evernote-mock.js
class MockNoteStore {
constructor() {
this.notebooks = [];
this.notes = [];
this.tags = [];
}
async listNotebooks() {
return this.notebooks;
}
async createNotebook(notebook) {
const created = {
...notebook,
guid: `notebook-${Date.now()}`,
created: Date.now(),
updated: Date.now()
};
this.notebooks.push(created);
return created;
}
async listTags() {
return this.tags;
}
async createNote(note) {
// Validate ENML
if (!note.content.includes('<?xml version="1.0"')) {
const error = new Error('Invalid ENML');
error.errorCode = 1;
error.parameter = 'Note.content';
throw error;
}
const created = {
...note,
guid: `note-${Date.now()}`,
created: Date.now(),
updated: Date.now()
};
this.notes.push(created);
return created;
}
async getNote(guid, withContent, withResources) {
const note = this.notes.find(n => n.guid === guid);
if (!note) {
const error = new Error('Note not found');
error.identifier = 'Note.guid';
error.key = guid;
throw error;
}
return note;
}
async findNotesMetadata(filter, offset, maxNotes, spec) {
let filtered = [...this.notes];
if (filter.words) {
const words = filter.words.toLowerCase();
filtered = filtered.filter(n =>
n.title.toLowerCase().includes(words) ||
n.content.toLowerCase().includes(words)
);
}
return {
notes: filtered.slice(offset, offset + maxNotes),
totalNotes: filtered.length
};
}
// Reset for tests
reset() {
this.notebooks = [];
this.notes = [];
this.tags = [];
}
}
class MockUserStore {
async getUser() {
return {
id: 12345,
username: 'testuser',
email: 'test@example.com',
name: 'Test User',
privilege: 1,
created: Date.now() - 86400000,
updated: Date.now(),
active: true,
accounting: {
uploadLimit: 62914560,
uploaded: 1000000,
uploadLimitEnd: Date.now() + 86400000 * 30
}
};
}
}
class MockEvernoteClient {
constructor(options = {}) {
this.options = options;
this._noteStore = new MockNoteStore();
this._userStore = new MockUserStore();
}
getNoteStore() {
return this._noteStore;
}
getUserStore() {
return this._userStore;
}
// For OAuth testing
getRequestToken(callbackUrl, callback) {
callback(null, 'mock-oauth-token', 'mock-oauth-secret');
}
getAuthorizeUrl(oauthToken) {
return `https://sandbox.evernote.com/OAuth.action?oauth_token=${oauthToken}`;
}
getAccessToken(oauthToken, oauthTokenSecret, oauthVerifier, callback) {
callback(null, 'mock-access-token', 'mock-access-secret', {
edam_expires: String(Date.now() + 31536000000)
});
}
}
module.exports = {
MockEvernoteClient,
MockNoteStore,
MockUserStore
};
// tests/unit/note-service.test.js
const { MockEvernoteClient } = require('../mocks/evernote-mock');
const NoteService = require('../../src/services/note-service');
describe('NoteService', () => {
let noteService;
let mockClient;
beforeEach(() => {
mockClient = new MockEvernoteClient();
noteService = new NoteService(mockClient.getNoteStore());
});
describe('createNote', () => {
it('should create a note with valid ENML', async () => {
const result = await noteService.createNote({
title: 'Test Note',
content: '<p>Test content</p>'
});
expect(result.guid).toBeDefined();
expect(result.title).toBe('Test Note');
});
it('should sanitize note title', async () => {
const result = await noteService.createNote({
title: 'Title\nwith\nnewlines',
content: '<p>Content</p>'
});
expect(result.title).not.toContain('\n');
});
it('should reject invalid ENML', async () => {
// Remove ENML wrapping for this test
const badService = {
noteStore: mockClient.getNoteStore(),
createNote: async (params) => {
const note = { title: params.title, content: params.content };
return mockClient.getNoteStore().createNote(note);
}
};
await expect(
badService.createNote({
title: 'Bad Note',
content: '<p>No ENML wrapper</p>'
})
).rejects.toMatchObject({
errorCode: 1,
parameter: 'Note.content'
});
});
});
describe('searchNotes', () => {
beforeEach(async () => {
// Seed test data
await noteService.createNote({ title: 'Meeting Notes', content: '<p>Q1 Review</p>' });
await noteService.createNote({ title: 'Todo List', content: '<p>Tasks</p>' });
});
it('should find notes by title', async () => {
const results = await noteService.search('meeting');
expect(results.notes.length).toBe(1);
expect(results.notes[0].title).toContain('Meeting');
});
});
});
// tests/integration/evernote-api.test.js
const Evernote = require('evernote');
// Skip if no credentials
const hasCredentials = process.env.EVERNOTE_ACCESS_TOKEN;
(hasCredentials ? describe : describe.skip)('Evernote API Integration', () => {
let client;
let noteStore;
let createdNoteGuids = [];
beforeAll(() => {
client = new Evernote.Client({
token: process.env.EVERNOTE_ACCESS_TOKEN,
sandbox: process.env.EVERNOTE_SANDBOX === 'true'
});
noteStore = client.getNoteStore();
});
afterAll(async () => {
// Clean up created notes
for (const guid of createdNoteGuids) {
try {
await noteStore.deleteNote(guid);
} catch (error) {
console.log(`Cleanup: Could not delete ${guid}`);
}
}
});
it('should authenticate and get user info', async () => {
const userStore = client.getUserStore();
const user = await userStore.getUser();
expect(user.id).toBeDefined();
expect(user.username).toBeDefined();
});
it('should list notebooks', async () => {
const notebooks = await noteStore.listNotebooks();
expect(Array.isArray(notebooks)).toBe(true);
expect(notebooks.length).toBeGreaterThan(0);
});
it('should create and retrieve a note', async () => {
const testTitle = `CI Test Note - ${Date.now()}`;
const content = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd">
<en-note><p>Integration test content</p></en-note>`;
const note = new Evernote.Types.Note();
note.title = testTitle;
note.content = content;
const created = await noteStore.createNote(note);
createdNoteGuids.push(created.guid);
expect(created.guid).toBeDefined();
expect(created.title).toBe(testTitle);
// Retrieve and verify
const retrieved = await noteStore.getNote(created.guid, true, false, false, false);
expect(retrieved.content).toContain('Integration test content');
});
it('should handle rate limits gracefully', async () => {
// This test intentionally makes multiple calls
// to verify rate limit handling works
const promises = [];
for (let i = 0; i < 5; i++) {
promises.push(noteStore.listNotebooks());
}
const results = await Promise.all(promises);
expect(results.every(r => Array.isArray(r))).toBe(true);
}, 30000);
});
# GitHub Actions Secrets Configuration
# Go to Settings > Secrets and variables > Actions
# Required secrets:
# EVERNOTE_CONSUMER_KEY - Your API consumer key
# EVERNOTE_CONSUMER_SECRET - Your API consumer secret
# EVERNOTE_SANDBOX_TOKEN - Sandbox developer token for testing
# Production secrets (separate environment):
# EVERNOTE_PROD_CONSUMER_KEY
# EVERNOTE_PROD_CONSUMER_SECRET
# Using environments for production
# .github/workflows/deploy.yml
jobs:
deploy-production:
runs-on: ubuntu-latest
environment: production # Requires approval
steps:
- uses: actions/checkout@v4
- name: Deploy
env:
EVERNOTE_CONSUMER_KEY: ${{ secrets.EVERNOTE_PROD_CONSUMER_KEY }}
EVERNOTE_CONSUMER_SECRET: ${{ secrets.EVERNOTE_PROD_CONSUMER_SECRET }}
run: npm run deploy
{
"scripts": {
"test": "jest",
"test:unit": "jest --selectProjects unit",
"test:integration": "jest --selectProjects integration --runInBand",
"test:coverage": "jest --coverage",
"lint": "eslint src/ tests/",
"lint:fix": "eslint src/ tests/ --fix",
"typecheck": "tsc --noEmit",
"ci": "npm run lint && npm run typecheck && npm run test:unit",
"ci:full": "npm run ci && npm run test:integration"
}
}
| Practice | Reason |
|---|---|
| Run unit tests on every PR | Fast feedback, no API usage |
| Limit integration tests | Preserve rate limits |
| Use sandbox for all CI | Never test against production |
| Store secrets securely | Protect API credentials |
| Clean up test data | Don't leave garbage in sandbox |
For deployment pipelines, see evernote-deploy-integration.